package com.xflib.tool.builder;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.SystemClock;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import com.xflib.framework.commandline.CommandLineArgs;
import com.xflib.framework.commandline.CommandLineExitNonZeroException;
import com.xflib.framework.common.ApplicationContext;
import com.xflib.framework.log.StreamGobbler;
import com.xflib.framework.ssh.HostProperties;
import com.xflib.framework.util.CommandLineUtil;
import com.xflib.framework.util.FileUtil;
import com.xflib.framework.util.ScpUtil;
import com.xflib.framework.util.SshUtil;
import com.xflib.tool.gitlab.GitlabUtil;
import com.xflib.tool.qscp.App;
import org.apache.commons.exec.CommandLine;
import org.gitlab4j.api.GitLabApiException;
import org.gitlab4j.api.models.Project;
import org.slf4j.Logger;

import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @author koradji@163.com
 * @date 2023/10/24
 */
public abstract class AbstractBuilder implements BuilderAdapter<BuilderArgs> {
    private Logger logger;

    protected BuilderFactory builderFactory;
    protected BuilderProperties builderProperties;
    protected HostProperties hostProperties;

    public AbstractBuilder(BuilderFactory builderFactory) {
        this.builderFactory = builderFactory;
        this.builderProperties = this.builderFactory.getBuilderProperties();
        this.hostProperties = this.builderFactory.getHostProperties();
    }

    @Override
    public void process(BuilderArgs args) {
        if (args.getType().equals(BuilderArgs.PROJECT)) {
            try {
                if (args.getStage().equals(BuilderArgs.STAGE_BUILD)) {
                    doBuild((BuilderProjectProperties) args.getData());
                } else if (args.getStage().equals(BuilderArgs.STAGE_PULL)) {
                    doPull((BuilderProjectProperties) args.getData());
                } else if (args.getStage().equals(BuilderArgs.STAGE_CLONE)) {
                    doClone((BuilderProjectProperties) args.getData());
                } else if (args.getStage().equals(BuilderArgs.STAGE_BRANCH_SWITCH)) {
                    doSwitchBranch((BuilderProjectProperties) args.getData(), args.getAttachData());
                } else if (args.getStage().equals(BuilderArgs.STAGE_BRANCH_TAG)) {
                    Map<String,String> queryMap = com.xflib.framework.util.StrUtil.QueryString.parse(args.getAttachData());
                    doTag((BuilderProjectProperties) args.getData(),queryMap.get("branch"), queryMap.get("tag"));
                } else if (args.getStage().equals(BuilderArgs.STAGE_BRANCH_DELETE_MERGED)) {
                    doDeletedMergedBranch((BuilderProjectProperties) args.getData());
                } else if (args.getStage().equals(BuilderArgs.STAGE_BRANCH_NEW)) {
                    doCreateBranch((BuilderProjectProperties) args.getData(),args.getAttachData());
                }
            } catch(Exception e){
                throw new RuntimeException(e.getMessage(),e.getCause());
            }
        }
    }

    int doDeletedMergedBranch(BuilderProjectProperties data){
        long startTime=SystemClock.now();
        StreamGobbler.appendWithDebug(this.logger, String.format(">>>>>> Exec: doDeletedMergedBranch from %s", data.getSourceUrl()));
        String gitWebUrl= App.yamlUtil.prefix("builder.git.webHome").getString();
        String token= App.yamlUtil.prefix("builder.git.token").getString();
        try {
            GitlabUtil gitlabUtil = GitlabUtil.create(gitWebUrl,token);
            Project project = gitlabUtil.getProjectByPath(data.getSourceUrl());
            // 打印 留存的分支信息
            StringBuilder branchStr=new StringBuilder(data.getSourceUrl()).append(" master(*)");
            gitlabUtil.deleteMergedBranches(project).forEach(branch -> {
                if(!branch.getName().equals("master")) branchStr.append("/").append(branch.getName());
            });
            StreamGobbler.appendWithDebug(this.logger, "Reserved branch list: " + branchStr.toString(),true);
            StreamGobbler.appendWithDebug(this.logger, String.format("[INFO] exec exit 0, total cost time %.1fs.", (System.currentTimeMillis() - startTime) / 1000.0), true);
            return 0;
        }catch (Exception e){
            StreamGobbler.appendWithDebug(this.logger, String.format("[ERROR] exec exit 1, total cost time %.1fs: %s", (System.currentTimeMillis() - startTime) / 1000.0, e.getMessage()), true);
        }
        return 1;
    }

    int doCreateBranch(BuilderProjectProperties data, String newBranch){
        long startTime=SystemClock.now();
        StreamGobbler.appendWithDebug(this.logger, String.format(">>>>>> Exec: doCreateBranch from %s", data.getSourceUrl()));
        String gitWebUrl= App.yamlUtil.prefix("builder.git.webHome").getString();
        String token= App.yamlUtil.prefix("builder.git.token").getString();
        try {
            GitlabUtil gitlabUtil = GitlabUtil.create(gitWebUrl,token);
            Project project = gitlabUtil.getProjectByPath(data.getSourceUrl());
            gitlabUtil.createBranch(project, newBranch);
            StreamGobbler.appendWithDebug(this.logger, String.format("[INFO] exec exit 0, total cost time %.1fs.", (System.currentTimeMillis() - startTime) / 1000.0), true);
            return 0;
        }catch (Exception e){
            StreamGobbler.appendWithDebug(this.logger, String.format("[ERROR] exec exit 1, total cost time %.1fs: %s", (System.currentTimeMillis() - startTime) / 1000.0, e.getMessage()), true);
        }
        return 1;
    }
    /**
     * 创建tag
     * @param data
     * @param branch
     * @param tag
     * @throws GitLabApiException
     */
    protected void doTag(BuilderProjectProperties data, String branch, String tag) throws GitLabApiException {
        long startTime=SystemClock.now();
        StreamGobbler.appendWithDebug(this.logger, String.format(">>>>>> Exec: createTag %s -b %s %s", tag, branch, data.getSourceUrl() ),true);
        String gitWebUrl= App.yamlUtil.prefix("builder.git.webHome").getString();
        String token= App.yamlUtil.prefix("builder.git.token").getString();
        try {
            GitlabUtil gitlabUtil = GitlabUtil.create(gitWebUrl,token);
            Project project = gitlabUtil.getProjectByPath(data.getSourceUrl());

            StringBuilder tagStr=new StringBuilder(data.getSourceUrl()).append(" ");
            gitlabUtil.createTag(project, branch, tag).forEach(tag1 -> {
                tagStr.append("/").append(tag1.getName());
            });
            StreamGobbler.appendWithDebug(this.logger, "Tag list: " + tagStr.toString(),true);
            StreamGobbler.appendWithDebug(this.logger, String.format("[INFO] exec exit 0, total cost time %.1fs.", (System.currentTimeMillis() - startTime) / 1000.0), true);
        }catch (Exception e){
            StreamGobbler.appendWithDebug(this.logger, String.format("[ERROR] exec exit 1, total cost time %.1fs: %s", (System.currentTimeMillis() - startTime) / 1000.0, e.getMessage()), true);
        }
    }

    /**
     * 拉取最新代码到本地
     * @param project
     * @throws IOException
     * @throws InterruptedException
     * @throws CommandLineExitNonZeroException
     * @throws ExecutionException
     * @throws TimeoutException
     */
    protected void doPull(BuilderProjectProperties project) throws IOException, InterruptedException, CommandLineExitNonZeroException, ExecutionException, TimeoutException {
        String cmdLine = getCommandPull(project);
        exec(cmdLine, new File(project.getSourceUrl()));
    }

    /**
     * clone代码到本地
     * @param project
     * @throws IOException
     * @throws InterruptedException
     * @throws CommandLineExitNonZeroException
     * @throws ExecutionException
     * @throws TimeoutException
     */
    protected void doClone(BuilderProjectProperties project) throws IOException, InterruptedException, CommandLineExitNonZeroException, ExecutionException, TimeoutException {
        String cmdLine = getCommandClone(project);
        if (!(new File(project.getSourceUrl()).exists())) {
            exec(cmdLine, new File(new File(project.getSourceUrl()).getParent()));
        }
        logger.warn("destination path already exists: {}", new File(project.getSourceUrl()).getPath());
    }

    /**
     * 切换代码分支
     * @param project
     * @param branch
     */
    protected void doSwitchBranch(BuilderProjectProperties project, String branch) {
        try {
            String cmdLine = getCommandCheckout(project, branch);
            exec(cmdLine, new File(project.getSourcePath()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 构建
     * @param project
     * @throws Exception
     */
    protected void doBuild(BuilderProjectProperties project) throws Exception {
        String cmdLine;

        File workDir = new File(project.getSourcePath());
        if (!workDir.exists()) {
            // Clone
            FileUtil.createMissingParentDirectories(workDir);
            exec(getCommandClone(project), workDir);
        } else if (!workDir.isDirectory()) {
            throw new IOException("File is exist: " + workDir);
        }

        String stage = "";
        long startTime = SystemClock.now();
        long middleTime = startTime;

        // pull
        stage = "pull";
        if (builderProperties.getDebug().isSkipPull()) {
            StreamGobbler.appendWithDebug(this.logger, String.format(">>>>>> Exec: %s skipped.", stage));
        } else {
            StreamGobbler.appendWithDebug(this.logger, "");
            exec(getCommandPull(project), workDir, false);
            middleTime = SystemClock.now();
        }

        // builder
        stage = "builder";
        if (builderProperties.getDebug().isSkipBuild()) {
            StreamGobbler.appendWithDebug(/*
            this.buffer.appendBufferWithDebug(*/this.logger, String.format("\n>>>>>> Exec: %s skipped.", stage));
        } else {
            //this.buffer.appendBufferWithDebug(this.logger, "");
            cmdLine = getCommandBuild(project);
            exec(cmdLine, workDir, builderProperties.getDebug().isNoOut());

            long middleTime2 = SystemClock.now();
            StreamGobbler.appendWithDebug(/*
            logger.info(*/this.logger,String.format("End pull cost time: %.1fs.", Float.valueOf(middleTime2 - middleTime)/1000));
            middleTime = middleTime2;
        }

        // compress
        stage = "compress";
        String srcPath = project.getTargetPath();
        String targetFileName = project.getProjectName().concat(".")
                .concat(DateUtil.format(new Date(), DatePattern.PURE_DATETIME_PATTERN))
                .concat(".zip");
        String targetPath = builderProperties.getGroup().stream().filter(g ->
                        g.getGroupName().equals(project.getGroupName())).findFirst()
                .orElse(new BuilderProjectGroupProperties()).getArchivePath();
        if (StrUtil.isBlank(targetPath)) {
            targetPath = project.getTargetPath().trim();
            if (!targetPath.endsWith(File.separator)) targetPath.concat(File.pathSeparator);
        }
        targetPath = targetPath.concat(targetFileName);
        StreamGobbler.appendWithDebug(/*
        this.buffer.appendBufferWithDebug(*/this.logger, String.format("\n>>>>>> Exec：compress: %s -> %s", srcPath, targetPath));

        ZipUtil.zip(srcPath, targetPath);

        long middleTime2 = SystemClock.now();
        StreamGobbler.appendWithDebug(this.logger,
                String.format("[INFO] End compress cost time: %.1fs.", Float.valueOf(middleTime2 - middleTime) / 1000));
        middleTime = middleTime2;

        // upload
        stage = "upload";
        if (builderProperties.getDebug().isSkipUpload()) {
            StreamGobbler.appendWithDebug(/*
            this.buffer.appendBufferWithDebug(*/this.logger, String.format(">>>>>> Exec: %s skipped.", stage));
        } else {
            StreamGobbler.appendWithDebug(/*
            this.buffer.appendBufferWithDebug(*/this.logger, "\n>>>>>> Exec: Scp");
            ScpUtil scpUtil = ScpUtil.create(hostProperties.getHost(), hostProperties.getPort(),
                    hostProperties.getUsername(), hostProperties.getPassword());
            scpUtil/*.setBuffer(this.buffer)*/.open().exec(targetPath).close();
            if (scpUtil.getExitStatus() != 0) {
                throw new IOException(project.getProjectName() + " Install failed on stage [upload]");
            }

            // deploy
            if(builderProperties.getDebug().isSkipDeploy()) {
                StreamGobbler.appendWithDebug(/*
                this.buffer.appendBufferWithDebug(*/this.logger,
                        String.format("\n>>>>>> Exec: %s skipped.", "deploy"));
            } else {
                StreamGobbler.appendWithDebug(/*
                this.buffer.appendBufferWithDebug(*/this.logger,"\n>>>>>> Exec: deploy");
                SshUtil sshUtil = SshUtil.create(hostProperties.getHost(),
                        hostProperties.getPort(), hostProperties.getUsername(), hostProperties.getPassword());
                sshUtil.setDebug(!builderProperties.getDebug().isNoOut())/*.setBuffer(this.buffer)*/.open().exec(getCommandDeploy(project, targetFileName)).close();
                if (sshUtil.getExitStatus() != 0) {
                    throw new IOException(project.getProjectName() + " Install failed on stage [deploy].");
                }
            }
        }
        StreamGobbler.appendWithDebug(/*
        this.buffer.appendBufferWithDebug(*/this.logger,
                String.format("[INFO] End All cost time: %.1fs.", Float.valueOf(SystemClock.now() - startTime)/1000));
    }

    protected void exec(String cmdLine, File workDir)
            throws IOException, InterruptedException, CommandLineExitNonZeroException,
            ExecutionException, TimeoutException {
        exec(cmdLine, workDir,!builderProperties.getDebug().isNoOut());
    }
    protected void exec(String cmdLine, File workDir, boolean noOut)
            throws IOException, InterruptedException, CommandLineExitNonZeroException,
            ExecutionException, TimeoutException {
        long startTime = System.currentTimeMillis();

        if (!cmdLine.isEmpty()) {

            CommandLineArgs args1 = new CommandLineArgs();
            FileUtil.createMissingDirectories(workDir);
            args1.setWorkDir(workDir);

            cmdLine = "cmd /c " + cmdLine;
            args1.setCmd(CommandLine.parse(cmdLine));

            StreamGobbler.appendWithDebug(this.logger, String.format(">>>>>> Exec: %s.", cmdLine));
            StreamGobbler.appendWithDebug(this.logger, String.format("[INFO] set echo %s", noOut?"off":"on"));

            int exitStatus = CommandLineUtil.exec(args1, !noOut, 15, TimeUnit.MINUTES);
            StreamGobbler.appendWithDebug(this.logger,String.format("[%s] exec exit %d, total cost time %.1fs",
                    exitStatus==0?"INFO":"ERROR", exitStatus, (System.currentTimeMillis() - startTime) / 1000.0));
            if (exitStatus != 0) {
                ApplicationContext.setTerminated(true);
                throw new RuntimeException("Executor has terminated when an error occurs.");
            }
        }
    }

    protected abstract String getCommandCheckout(BuilderProjectProperties properties, String branch);

    protected abstract String getCommandClone(BuilderProjectProperties properties);

    protected abstract String getCommandPull(BuilderProjectProperties properties);

    protected abstract String getCommandBuild(BuilderProjectProperties properties);

    protected abstract String getCommandDeploy(BuilderProjectProperties project,String ziFileName);

    public void setLogger(Logger logger) {
        this.logger = logger;
    }

}
